﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Threading;
using PT.Common;
using PT.NoteDetection;
using PT.NoteDetection.Midi;
using NAudio.Wave;
using PT.Timing;
using PT.Timing.Util;
using PT.UI.Windows.Audio;
using PT.Patterns;

namespace PT.UI.Windows {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : INotifyPropertyChanged {

        const double FromAngle = 140;
        const double ToAngle = 220;
        const double RestAngle = 180;

        const int ToneLength = 10;
        const int InitialTempo = 90;

        const int MinTempo = 50;
        const int MaxTempo = 250;

        readonly Metronome m;
        readonly DoubleAnimation toAnimation;
        readonly DoubleAnimation fromAnimation;
        readonly DoubleAnimation prepareStartAnimation;
        readonly DoubleAnimation cancelStartAnimation;

        readonly ColorAnimation beatColorAnimation;
        readonly ColorAnimation noteColorAnimation;

        readonly ToneGenerator toneGenerator;
        readonly DetectionController detector;
        readonly AccuracyEvaluator accuracyEvaluator;

        WaveOut playback;

        bool isAnimatingTo;

        long? lastQuarterNoteTimestamp;
        long? tapTempoLastTap;
        public event PropertyChangedEventHandler PropertyChanged;

        public int Tempo {
            get { return m.Tempo; }
            set {
                var tempo = Math.Max(MinTempo, Math.Min(MaxTempo, value));

                var animationDuration = new Duration(TempoMath.CalculatePeriod(tempo));
                fromAnimation.Duration = animationDuration;
                toAnimation.Duration = animationDuration;

                m.Tempo = tempo;
                detector.Tempo = tempo;
                
                OnPropertyChanged("Tempo");
            }
        }

        public MainWindow() {
            m = new Metronome();
            m.Click += MetronomeOnClick;

            Closed += MainWindowOnClosed;

            toAnimation = new DoubleAnimation {From = FromAngle, To = ToAngle};
            fromAnimation = new DoubleAnimation {From = ToAngle, To = FromAngle};

            prepareStartAnimation = new DoubleAnimation { To = FromAngle, Duration = new Duration(new TimeSpan(0, 0, 0, 0, 100)) };
            cancelStartAnimation = new DoubleAnimation { To = RestAngle, Duration = new Duration(new TimeSpan(0, 0, 0, 0, 100)) };

            beatColorAnimation = new ColorAnimation(Color.FromRgb(0x88, 0x88, 0x88), Colors.Red, new Duration(new TimeSpan(0,0,0,0,100))) {
                AutoReverse = true, EasingFunction = new PowerEase { EasingMode = EasingMode.EaseOut }
            };
            noteColorAnimation = new ColorAnimation(Color.FromRgb(0x88, 0x88, 0x88), Colors.Purple, new Duration(new TimeSpan(0, 0, 0, 0, 100))) {
                AutoReverse = true, EasingFunction = new PowerEase { EasingMode = EasingMode.EaseOut }
            };

            toneGenerator = ToneGenerator.CreateQuarterNoteToneGenerator();
            detector = new DetectionController();
            detector.NoteDetected += DetectorOnNoteDetected;

            accuracyEvaluator = new AccuracyEvaluator(TimeService.TicksPerQuarterNote);

            Tempo = InitialTempo;

            InitializeComponent();
        }


        void DetectorOnNoteDetected(object sender, NoteDetectedEventArgs e) {
            Dispatcher.Invoke(DispatcherPriority.Render, new Action<NoteDetectedEventArgs>(UpdateNoteDetectionUi), e);
        }

        void UpdateNoteDetectionUi(NoteDetectedEventArgs args) {
            noteLed.Fill.BeginAnimation(SolidColorBrush.ColorProperty, noteColorAnimation, HandoffBehavior.SnapshotAndReplace);
           
            var intensity = (double)args.Note.Velocity / sbyte.MaxValue;

            DisplayNoteVelocityIndicator(intensity);

            if (m.IsRunning && lastQuarterNoteTimestamp.HasValue && detector.StartCaptureTimestamp.HasValue) {
                var timeOffsetTicks = (lastQuarterNoteTimestamp.Value - detector.StartCaptureTimestamp.Value) * Consts.TicksPerSecond / Stopwatch.Frequency;
                var timeOffsetMidi = detector.TimeService.CalculateMidiTime(timeOffsetTicks);

                var noteMidiTime = args.Note.AbsoluteTime - timeOffsetMidi;

                if (selectedPattern != null){
                    var elapsedPatterns = (displayablePattern - 1);
                    if (elapsedPatterns < 0) {
                        elapsedPatterns = selectedPatternParts.Length - 1;
                    }

                    var midiTimeFromStart = elapsedPatterns * TimeService.TicksPerQuarterNote + noteMidiTime;
                    var closestNote = accuracyEvaluator.Evaluate(midiTimeFromStart, selectedPattern);

                    DisplayNoteAccuracyIndicator(closestNote.Item2);

                }

                var a = Math.Abs(noteMidiTime) * (ToAngle - FromAngle) / TimeService.TicksPerQuarterNote;
                var angle = isAnimatingTo ? FromAngle + a : ToAngle - a;

                DisplayDetectedNoteIndicator(angle, intensity);
                Debug.WriteLine("Note! Midi velocity {0}, intensity {1}", args.Note.Velocity, intensity);
            }
        }

        void DisplayNoteAccuracyIndicator(Rational delta) {

            // Calculate X
            var x = AccuracyCanvas.ActualWidth * 4 * delta.Numerator / delta.Denominator + AccuracyCanvas.ActualWidth / 2;

            Debug.WriteLine("Accuracy: delta = {0}/{1}, x = {2}", delta.Numerator, delta.Denominator, x);

            var indicator = new Line {X1 = x, X2 = x, Y1 = 0, Y2 = AccuracyCanvas.ActualHeight, Stroke = new SolidColorBrush(Colors.AliceBlue)};
            var fadeAnimation = new DoubleAnimation(1, 0, TempoMath.CalculatePeriod(Tempo / 4)) {
                EasingFunction = new PowerEase {EasingMode = EasingMode.EaseOut}
            };
            fadeAnimation.Completed += (s, args) => AccuracyCanvas.Children.Remove(indicator);

            AccuracyCanvas.Children.Add(indicator);

            indicator.BeginAnimation(OpacityProperty, fadeAnimation);
        }

        void DisplayNoteVelocityIndicator(double velocity) {
            // Calculate Y

            var y = DynamicsCanvas.ActualHeight - velocity * DynamicsCanvas.ActualHeight;

            var indicator = new Line {X1 = 0, X2 = DynamicsCanvas.ActualWidth, Y1 = y, Y2 = y, Stroke = new SolidColorBrush(Colors.AliceBlue)};
            var fadeAnimation = new DoubleAnimation(1, 0, TempoMath.CalculatePeriod(Tempo / 4)) {
                EasingFunction = new PowerEase { EasingMode = EasingMode.EaseOut }
            };

            Debug.WriteLine("Vel: velocity: {0}, y = {1}", velocity, y);

            fadeAnimation.Completed += (s, args) => AccuracyCanvas.Children.Remove(indicator);

            DynamicsCanvas.Children.Add(indicator);

            indicator.BeginAnimation(OpacityProperty, fadeAnimation);
        }


        void DisplayDetectedNoteIndicator(double angle, double intensity) {
            const double diameter = 6;

            var indicator = CreateNoteIndicator(angle, diameter, 2 * diameter, intensity);

            var fadeAnimation = new DoubleAnimation(0.1, 1, new Duration(TempoMath.CalculatePeriod(Tempo * 2))) {
                AutoReverse = true,
                EasingFunction = new PowerEase { EasingMode = EasingMode.EaseOut },
            };

            var animation = new Storyboard();
            animation.Children.Add(fadeAnimation);
            Storyboard.SetTargetProperty(animation, new PropertyPath(OpacityProperty));
            Storyboard.SetTarget(animation, indicator);

            animation.Completed += (s, args) => IndicatorCanvas.Children.Remove(indicator);
            animation.Begin();
        }
        
       
        UIElement DisplayExpectedNoteIndicator(double angle, bool animate, double intensity, object tag) {
            const double diameter = 7;
            const double spreadOffset = -2 * diameter;
            var spreadStart = IndicatorScale.Height / 2 + spreadOffset;
            var spreadEnd = (IndicatorScale.Height / 2);

            // Visual elements
            var indicator = CreateNoteIndicator(angle, diameter * intensity, spreadOffset, intensity);

            var transform = (TranslateTransform) ((TransformGroup) indicator.RenderTransform).Children[0];
            var transformName = "ExpectedNoteTransform" + transform.GetHashCode();
            RegisterName(transformName, transform);

            // Storyboard for managing animations
            var storyboard = new Storyboard();
            var storyboardKey = "ExpectedNoteStoryboard" + storyboard.GetHashCode();
            storyboard.Completed += delegate {
             
                IndicatorCanvas.Children.Remove(indicator);
                Resources.Remove(storyboardKey);
                UnregisterName(transformName);
            };
            Resources.Add(storyboardKey, storyboard);

            var quarternoteDuration = new Duration(TempoMath.CalculatePeriod(Tempo));

            if (animate) {
                var translateAnimation = new DoubleAnimation(spreadStart, spreadEnd, quarternoteDuration);
                Storyboard.SetTargetName(translateAnimation, transformName);
                Storyboard.SetTargetProperty(translateAnimation, new PropertyPath(TranslateTransform.YProperty));
                storyboard.Children.Add(translateAnimation);

                var fadeAnimation = new DoubleAnimation(1, 0, new Duration(TempoMath.CalculatePeriod((int) (1.5 * Tempo)))) {BeginTime = quarternoteDuration.TimeSpan};
                Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath(OpacityProperty));
                Storyboard.SetTarget(fadeAnimation, indicator);
                storyboard.Children.Add(fadeAnimation);
            } else {
                // Create dummy animation (with no visible effect) so that the indicator gets removed at the end of Duration
                var dummyAnimation = new DoubleAnimation(1, 1, quarternoteDuration);
                Storyboard.SetTargetProperty(dummyAnimation, new PropertyPath(OpacityProperty));
                Storyboard.SetTarget(dummyAnimation, indicator);
                storyboard.Children.Add(dummyAnimation);
            }

            // Start animations
            storyboard.Begin();
            Debug.WriteLine("Animation {0} started {1}", storyboard.GetHashCode(), Stopwatch.GetTimestamp());

            return indicator;
        }

        Ellipse CreateNoteIndicator(double angle, double diameter, double spreadOffset, double intensity) {
            var top = Canvas.GetTop(Arrow) + diameter / 2;
            var left = Canvas.GetLeft(Arrow) + Arrow.Width / 2 - diameter / 2;

            var spreadRadius = IndicatorScale.Height / 2 + spreadOffset;

            var transform = new TransformGroup();
            transform.Children.Add(new TranslateTransform(0, spreadRadius));
            transform.Children.Add(new RotateTransform(angle));

            var colorValue = (byte)((0.5f + intensity / 2) * byte.MaxValue);
            
            var indicator = new Ellipse {
                Width = diameter,
                Height = diameter,
                Fill = new SolidColorBrush(Color.FromRgb(colorValue, colorValue, colorValue)),
                RenderTransform = transform,
                RenderTransformOrigin = new Point(0.5, 0.5)
            };

            Canvas.SetTop(indicator, top);
            Canvas.SetLeft(indicator, left);

            IndicatorCanvas.Children.Add(indicator);

            return indicator;
        }

        void MetronomeOnClick(object sender, ClickEventArgs e) {
            if (Rational.Quarter.Equals(e.NoteDuration)) {
                lastQuarterNoteTimestamp = e.ClickTimeTicks;
            }
            toneGenerator.GenerateTone(ToneLength);

            Debug.WriteLine("Click: " + e.ClickTimeTicks + " RaisedTime: " + e.EventRaisedTime + " Delta: " + (e.EventRaisedTime - e.ClickTimeTicks) );
            Dispatcher.Invoke(DispatcherPriority.Render, new Action<ClickEventArgs>(UpdateUiOnQuarternote), e);
        }

        UIElement nextQuarternoteIndicator;

        void UpdateUiOnQuarternote(ClickEventArgs args) {

            beatLed.Fill.BeginAnimation(SolidColorBrush.ColorProperty, beatColorAnimation, HandoffBehavior.SnapshotAndReplace);
            Rotation.BeginAnimation(RotateTransform.AngleProperty, isAnimatingTo ? fromAnimation : toAnimation);
            isAnimatingTo = !isAnimatingTo;

            if (nextQuarternoteIndicator != null) {
                IndicatorCanvas.Children.Remove(nextQuarternoteIndicator);
            }

            // Calculate expected beats for next quarternote
            if (selectedPattern != null && selectedPattern.Count != 0) {
                var part = selectedPatternParts[displayablePattern];
                // Debug.WriteLine("Displaying pattern part: " + displayablePattern);
                var noteInstanceIndex = 0;
                for (int i = 0; i < displayablePattern; ++i ) {
                    noteInstanceIndex += selectedPatternParts[i].Count;
                }

                foreach (var n in part) {
                    // We're displaying a single quarternote instead of a full 4/4 pattern.
                    // Therefore each note duration must be multiplied by 4.
                    var a = (ToAngle - FromAngle) * 4 * offset.Numerator / offset.Denominator;
                    var angle = isAnimatingTo ? FromAngle + a : ToAngle - a;

                    DisplayExpectedNoteIndicator(angle, true, n.Velocity, instances[noteInstanceIndex]);
                    noteInstanceIndex = (noteInstanceIndex + 1) % instances.Length;

                    offset += n.Duration;
                }

                offset -= Rational.Quarter;
                displayablePattern = (displayablePattern + 1) % selectedPatternParts.Length;

                if (offset.Numerator == 0) {
                    var note = selectedPatternParts[displayablePattern][0];

                    // The next quarternote is visible on the indicator. Display without animation -
                    // it will be animated on the next quarternote.
                    nextQuarternoteIndicator = DisplayExpectedNoteIndicator(
                        isAnimatingTo ? ToAngle : FromAngle, false, note.Velocity, instances[noteInstanceIndex]);
                }
            }
        }


        Pattern selectedPattern;
        Pattern[] selectedPatternParts;
        PatternNoteInstance[] instances;
        int displayablePattern;
        Rational offset;

        class PatternNoteInstance {
            public Note Note { get; set; }
            public int PatternPart { get; set; }
            public int PatternNoteIndex { get; set; }
        }

        void PatternDropDownOnSelectionChanged(object sender, SelectionChangedEventArgs e) {
            selectedPattern = (Pattern)PatternDropDown.SelectedItem;

            if (selectedPattern != null && selectedPattern.Count != 0) {
                displayablePattern = 0;
                selectedPatternParts = selectedPattern.Split(Rational.Quarter);

                instances = new PatternNoteInstance[selectedPattern.Count];
                for (int i = 0, I = 0; i < selectedPatternParts.Length; ++i) {
                    for (int j = 0; j < selectedPatternParts[i].Count; ++j) {
                        instances[I++] = new PatternNoteInstance {Note = selectedPatternParts[i][j], PatternPart = i, PatternNoteIndex = j};
                    }
                }


                offset = new Rational(0, 1);
            }
        }

        void OnPropertyChanged(string propertyName) {
            var subscribers = PropertyChanged;

            if (subscribers != null) {
                subscribers(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        void MainWindowOnClosed(object sender, EventArgs e) {
            if (playback != null) {
                playback.Stop();
                playback.Dispose();
                playback = null;
            }

            if (m != null) {
                m.Stop();
            }

            if (detector != null) {
                detector.Stop();
            }
        }

        void DetectorControlButtonOnClick(object sender, RoutedEventArgs e) {
            if (!detector.IsRunning) {
                detector.Start();
                DetectorControlButton.Content = "Stop";
                Debug.WriteLine("Capturing started @: " + Stopwatch.GetTimestamp());
            }
            else {
                detector.Stop();
                DetectorControlButton.Content = "Start";
            }
        }

        void MetronomeControlButtonOnClick(object sender, RoutedEventArgs e) {
            if (!m.IsRunning){
                MetronomeControlButton.Content = "Stop";

                playback = new WaveOut {DesiredLatency = 50};
                playback.Init(toneGenerator);
                playback.Play();
                
                m.Start();
            }
            else {
                m.Stop();

                isAnimatingTo = false;
                MetronomeControlButton.Content = "Start";

                playback.Stop();
                playback.Dispose();
                playback = null;
            }
        }

        void TempoIncreaseButtonOnClick(object sender, RoutedEventArgs e) {
            var incr = Keyboard.IsKeyDown(Key.RightCtrl) || Keyboard.IsKeyDown(Key.LeftCtrl) ? 10 : 1;
            Tempo += incr;
        }

        void TempoDecreaseButtonOnClick(object sender, RoutedEventArgs e) {
            var incr = Keyboard.IsKeyDown(Key.RightCtrl) || Keyboard.IsKeyDown(Key.LeftCtrl) ? 10 : 1;
            Tempo -= incr;
        }

        void MetronomeControlButtonOnMouseEnter(object sender, MouseEventArgs e) {
            if (!m.IsRunning) {
                Rotation.BeginAnimation(RotateTransform.AngleProperty, prepareStartAnimation, HandoffBehavior.SnapshotAndReplace);
            }
        }

        void MetronomeControlButtonOnMouseLeave(object sender, MouseEventArgs e) {
            if (!m.IsRunning) {
                Rotation.BeginAnimation(RotateTransform.AngleProperty, cancelStartAnimation, HandoffBehavior.SnapshotAndReplace);
            }
        }

        void TapTempoOnMouseDown(object sender, MouseButtonEventArgs e) {
            var tapTime = Stopwatch.GetTimestamp();
            if (tapTempoLastTap.HasValue) {
                var proposedTempo = (int)(60 * Stopwatch.Frequency /(tapTime - tapTempoLastTap));
                if (MinTempo <= proposedTempo && proposedTempo <= MaxTempo) {
                    Tempo = proposedTempo;
                }
            }

            tapTempoLastTap = tapTime;
        }
    }
}
